﻿using PersistentObject.Common.Attributes;
using PersistentObject.Common.DataConvertors;
using PersistentObject.Common.Exceptions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;

namespace PersistentObject.Common.Schema
{
    public class SchemaHelper
    {
        public SchemaConfig Config { get; set; }
        public SchemaHelper(SchemaConfig config)
        {
            Config = config;
        }

        public SchemaHelper()
        {
            Config = new SchemaConfig();
        }
        /// <summary>
        /// Indicate the default max deep of same types in a table structure
        /// </summary>
        public int DefaultDeep = 2;

        /// <summary>
        /// This dictionary stores the mapping relationships between types and table infos
        /// </summary>
        Dictionary<Type, TableInfo> TableInfoDictionary = new Dictionary<Type, TableInfo>();
        object TableInfoLocker = new object();

        /// <summary>
        /// Create the table info by a T type
        /// </summary>
        /// <typeparam name="T">Indicate the type</typeparam>
        /// <returns>Return the info of the table</returns>
        public TableInfo CreateTableInfo<T>()
        {
            Type type = typeof(T);
            return CreateTableInfo(type);
        }

        /// <summary>
        /// Create the table info by a type
        /// </summary>
        /// <param name="type">Indicate the type</param>
        /// <returns>Return the info of the table</returns>
        public TableInfo CreateTableInfo(Type type)
        {
            lock (TableInfoLocker)
            {
                if (Config != null)
                {
                    type = Config.GetTypeMapping(type);
                }

                #region Search whether there is existing
                if (TableInfoDictionary.ContainsKey(type))
                {
                    return TableInfoDictionary[type];
                }
                #endregion

                #region Create info and put into dictionary
                TableInfo table = new TableInfo()
                {
                    Name = type.Name,
                    Type = type,
                };
                TableInfoDictionary.Add(type, table);
                #endregion

                #region Retrieve persist attribute
                PersistAttribute PA = (PersistAttribute)Attribute.GetCustomAttribute(type, typeof(PersistAttribute));
                if (PA != null)
                {
                    table.Name = PA.Name;
                }
                if (Config != null)
                {
                    var mapping = Config.GetTableMapping(type);
                    if (mapping != null)
                    {
                        table.Name = mapping;
                    }
                }
                #endregion

                #region Retrieve non persist attribute
                NonPersistAttribute NPA = (NonPersistAttribute)Attribute.GetCustomAttribute(type, typeof(NonPersistAttribute));
                if (NPA != null)
                {
                    table.NonPersist = true;
                }
                #endregion

                #region Retrieve columns
                PropertyInfo[] properties = type.GetProperties();
                foreach (PropertyInfo pInfo in properties)
                {
                    ColumnInfo column = CreateColumnInfo(table, pInfo);
                    if (column == null) continue;
                    if (column.IsKeyColumn && table.KeyColumn != null)
                    {
                        throw new MultipleKeyException(string.Format("Multiple key columns found in class {0}.", table.Type.FullName));
                    }
                    else if (column.IsKeyColumn && table.KeyColumn == null)
                    {
                        table.KeyColumn = column;
                    }
                    else
                    {
                        table.ColumnList.Add(column);
                    }
                }
                #endregion

                #region No key column, then throw exception
                if (table.KeyColumn == null)
                {
                    throw new LackOfKeyException(string.Format("Lack of key column in class {0}.", table.Type.FullName));
                }
                #endregion

                return table;
            }
        }

        /// <summary>
        /// The helper function to create column info
        /// </summary>
        /// <param name="table">Indicate the table info</param>
        /// <param name="property">Indicate the property name of the type</param>
        /// <returns></returns>
        ColumnInfo CreateColumnInfo(TableInfo table, PropertyInfo property)
        {
            NonPersistAttribute NPA = (NonPersistAttribute)Attribute.GetCustomAttribute(property, typeof(NonPersistAttribute));
            if (NPA != null) return null;

            PersistAttribute persistAttr = (PersistAttribute)Attribute.GetCustomAttribute(property, typeof(PersistAttribute));
            string PersistName = persistAttr == null ? property.Name : persistAttr.Name;

            DbAccessAttribute dbaAttr = (DbAccessAttribute)Attribute.GetCustomAttribute(property, typeof(DbAccessAttribute));
            bool Save = dbaAttr == null ? true : dbaAttr.Save;
            bool Load = dbaAttr == null ? true : dbaAttr.Load;

            DataConvertor convertor = null;
            ConvertorAttribute convertorAttr = (ConvertorAttribute)Attribute.GetCustomAttribute(property, typeof(ConvertorAttribute));
            if(convertorAttr != null)
            {
                convertor = convertorAttr.Convertor;
            }

            ColumnInfo column = new ColumnInfo()
            {
                TableInfo = table,
                Property = property,
                Name = PersistName,
                Convertor = convertor,
                Save = Save,
                Load = Load,
            };

            KeyAttribute keyAttr = (KeyAttribute)Attribute.GetCustomAttribute(property, typeof(KeyAttribute));
            if (keyAttr != null)
            {
                column.IsKeyColumn = true;
                column.IsAutoGenerated = keyAttr.IsIdentity;
            }

            ForeignKeyAttribute foreignAttr = (ForeignKeyAttribute)Attribute.GetCustomAttribute(property, typeof(ForeignKeyAttribute));
            if (foreignAttr != null)
            {
                column.IsForeignKeyColumn = true;
                column.ForeignTableInfo = CreateTableInfo(property.PropertyType);
            }

            return column;
        }

        public Table CreateTable<T>()
        {
            return CreateTable<T>(DefaultDeep);
        }

        public Table CreateTable(Type type)
        {
            return CreateTable(type, DefaultDeep);
        }

        public Table CreateTable<T>(int deep)
        {
            Type type = typeof(T);
            return CreateTable(type, deep);
        }

        public Table CreateTable(Type type, int deep)
        {
            TableCreator creator = new TableCreator(this, type, deep);
            Table table = creator.Create(false);
            return table;
        }

        public Table CreateInnerTable(Type type)
        {
            return CreateInnerTable(type, DefaultDeep);
        }

        public Table CreateInnerTable(Type type, int deep)
        {
            TableCreator creator = new TableCreator(this, type, deep);
            Table table = creator.Create(true);
            return table;
        }

        internal class TableCreator
        {
            Dictionary<string, int> TableSequenceDict = new Dictionary<string, int>();
            Dictionary<string, int> ColumnSequenceDict = new Dictionary<string, int>();
            //Dictionary<Type, int> DeepDict = new Dictionary<Type, int>();
            Queue<Column> ForeignColumnQueue = new Queue<Column>();

            SchemaHelper Helper { get; set; }
            public Type Type { get; set; }
            public int MaxDeep { get; set; }

            internal TableCreator(SchemaHelper helper, Type type, int maxDeep)
            {
                this.Helper = helper;
                Type = type;
                MaxDeep = maxDeep;
                //DeepDict.Add(type, -1);
            }

            public Table Create(bool isInnerExists)
            {
                TableInfo info = Helper.CreateTableInfo(Type);
                Table table = CreateTable(info);
                table.IsInnerExists = isInnerExists;
                foreach(var column in table.ColumnList.FindAll((item)=>item.IsForeignKeyColumn))
                {
                    ForeignColumnQueue.Enqueue(column);
                }

                while (ForeignColumnQueue.Count() > 0)
                {
                    Column foreignColumn = ForeignColumnQueue.Dequeue();
                    int deep = GetForeignDeep(foreignColumn);
                    if (deep <= MaxDeep)
                    {
                        Table foreignTable = CreateTable(foreignColumn.ForeignTableInfo);
                        foreignTable.IsInnerExists = isInnerExists;
                        foreignTable.ForeignColumn = foreignColumn;

                        foreignColumn.ForeignTable = foreignTable;
                        foreignColumn.IsForeignKeyColumn = foreignTable != null;

                        if (foreignTable != null)
                        {
                            foreach (var column in foreignTable.ColumnList.FindAll((item) => item.IsForeignKeyColumn))
                            {
                                ForeignColumnQueue.Enqueue(column);
                            }
                        }
                    }
                    else
                    {
                        foreignColumn.ForeignTable = null;
                        foreignColumn.IsForeignKeyColumn = false;
                    }
                }

                return table;
            }

            Table CreateTable(TableInfo info)
            {
                Table table = null;
                int tableSequence = GetTableSequence(info);
                table = new Table(info, tableSequence);
                int keyColumnSequence = GetColumnSequence(info.KeyColumn);
                table.KeyColumn = new Column(table, info.KeyColumn, keyColumnSequence);
                foreach (var item in info.ColumnList)
                {
                    Column column = CreateColumn(table, item);
                    table.ColumnList.Add(column);
                }
                return table;
            }

            Column CreateColumn(Table table, ColumnInfo info)
            {
                int columnSequence = GetColumnSequence(info);
                Column column = new Column(table, info, columnSequence);
                return column;
            }

            private int GetTableSequence(TableInfo info)
            {
                int sequence = 0;
                if (!TableSequenceDict.ContainsKey(info.Type.FullName))
                {
                    TableSequenceDict.Add(info.Type.FullName, 0);
                }
                else
                {
                    TableSequenceDict[info.Type.FullName]++;
                }
                sequence = TableSequenceDict[info.Type.FullName];
                return sequence;
            }

            private int GetColumnSequence(ColumnInfo info)
            {
                int sequence = 0;
                if (!ColumnSequenceDict.ContainsKey(info.Property.Name))
                {
                    ColumnSequenceDict.Add(info.Property.Name, 0);
                }
                else
                {
                    ColumnSequenceDict[info.Property.Name]++;
                }
                sequence = ColumnSequenceDict[info.Property.Name];
                return sequence;
            }

            private int GetForeignDeep(Column column)
            {
                if(column.IsForeignKeyColumn)
                {
                    int deep = 0;
                    Type searchingType = column.ForeignTableInfo.Type;
                    Column foreignColumn = column.Table.ForeignColumn;
                    while(foreignColumn != null)
                    {
                        if(foreignColumn.ForeignTable.Type == searchingType)
                        {
                            deep++;
                        }
                        foreignColumn = foreignColumn.Table.ForeignColumn;
                    }
                    return deep;
                }
                else
                {
                    return -1;
                }
            }
        }


    }
}
